<?php
/**
 * This class implements a connection to Facebook
 *
 * @author Thomas Kappel
 * @copyright Copyright (c) 2008
 * @version $Id: Default.php 2056 2008-03-16 13:03:00Z tkappel $
 */

  function aboutFacebook() {
    return array(
      "name" => "Facebook",
    );
  }
  
class Facebook extends Network_Abstract {
  public $_Name            = "Facebook";
  
  public $user_id          = null;
  public $activation_link  = false;
  public $available_fields = "uid,about_me,activities,affiliations,birthday,books,current_location,education_history,first_name,has_added_app,hometown_location,hs_info,interests,is_app_user,last_name,meeting_for,meeting_sex,movies,music,name,notes_count,pic,pic_big,pic_small,pic_square,political,profile_update_time,quotes,relationship_status,religion,sex,significant_other_id,status,timezone,tv,wall_count,work_history";
  public $messages         = array();
  public $contacts         = false;
  public $me               = false;
  public $account          = false;
  protected $session_key   = false;
  protected $last_call_id  = 0;
  
  protected $api_key         = "097aff6a504bb1d4a087f7e16f58433e";
  protected $secret          = "b056ce3426ac2e97a671fc6b5910d513";
  protected $uri             = "https://api.facebook.com/restserver.php";
  public $facebook_client = false;

  public function supports($method) {
    switch($method) {
      case "find_friends_of_friends": return false;
      case "multiple_accounts"      : return false; // one person should have only one account 
      case "save_friends_to_network": return false; // only getters are supported
      case "search_members"         : return true;  // find account of a person you already know from other networks
      case "disconnect"             : return true;
      default                       : return false;
    }
  }
  
  /**
   * connect to Facebook
   * @param Account
   * @param array API-Key, Secret
   */
  public function __construct($account = false, $keys = null) {
    $this->NetworkName="Facebook";
    $this->account = $account ? $account : false;
    $session = SocializrSession::getInstance()->getAttribute('facebook_session');
    $this->server_addr  = self::get_facebook_url('api') . '/restserver.php';
    if(is_array($keys)) {
      if(isset($keys['api_key'])) $this->api_key = $keys['api_key'];
      if(isset($keys['secret']))  $this->secret  = $keys['secret'];
    }
  }
  
  public function getServiceHomepage() {
    return "http://www.facebook.com";
  }
  
  public function getAccountingRequirements() {
    return array(
      "mandatory" => array("user"),
      "optional"  => array(),
    ); // none required due to redirection
  }

  /*
   * @returns array: array(
   *   "code"        => http status code
   *   "message"     => message to display (optional)
   *   "template"    => template to be used
   *   "action"      => action to be executed (useful only with iframes, forms or on a reload)
   *   "reload"      => boolean: Do we need to refresh the whole page?
   *   "user"        => array containing current user information
   */
  public function connect($URI="", $user="", $pass="") {
    // 0. initialize output variables
    $code        = 404;
    $message     = "";
    $template    = "";
    $action      = false;
    $reload      = false;
    $user        = false;

    if(isset($_GET["auth_token"])) { // we got a new session - let's use it!
      $result = $this->call_method('facebook.auth.getSession', array('auth_token'=>$_GET["auth_token"]));
      if(isset($result['session_key'])) {
        $this->session_key = $result['session_key'];
        SocializrSession::getInstance()->setAttribute('facebook_session', $this->session_key);

        $result = $this->call_method('facebook.users.getLoggedInUser', array());
        if(is_array($result) and false == isset($result['error_code'])) {
          $this->user_id = $result;
          $user = $result;
        }
        $code = 200;
        $action    = SocializrUtils::getBaseUri();
        $template  = "message";
        $message   = "OK";
        $reload    = true;
        
        return array(
          "code"        => $code,
          "message"     => $message,
          "template"    => $template,
          "action"      => $action,
          "reload"      => $reload,
          "user"        => $user
        );
      }
    }

    // 1. ask Facebook for current session
    $result = $this->call_method('facebook.users.getLoggedInUser', array());
    if(is_array($result)) {
      if(isset($result['error_code'])) {
        if($result['error_code']==FacebookAPIErrorCodes::API_EC_PARAM_SESSION_KEY) { // invalid session
          $code = 404;
        }
      } else { // we are still logged in
        $this->user_id = $result;
        $user = $result;
        $code = 200;
        $action    = SocializrUtils::getBaseUri();
        $template  = "message";
        $message   = "OK";
      }
    }
    // 2. no session, so we need to log in
    if($code == 404) {
      $account   = $this->account;
      $next      = SocializrUtils::getConnectorUrl();
      $action    = self::get_facebook_url().'/login.php?v=1.0&api_key='.$this->api_key.'&next='.urlencode($next);
      $code      = 401; // authorisation required
      $template  = "iframe";
    }
    // return results
    return array(
      "code"        => $code,
      "message"     => $message,
      "template"    => $template,
      "action"      => $action,
      "reload"      => $reload,
      "user"        => $user
    );
  }

  public static function getInstance() {
      if (!isset(self::$_instance) || null === self::$_instance) return false;
      return self::$_instance;
  }
  
  public function getSessionKey() {
    return $this->session_key;
  }

  public function require_connect_button() {
    return true;
  }

  public function connected() {
    $result = $this->call_method('facebook.users.getLoggedInUser', array());
    if(!is_array($result) && strlen($result)) {
      $this->user_id = $result;
      return true;
    }
    return false;
  }

  public function disconnect() {
    return $zend->restPost("http://www.facebook.com/logout.php?confirm=1");
  }
  
  function search($contact) {
    if(!$this->connected()) return "not connected";
    // Facebook allows to search for people by name and, of course, by user id
    if((!isset($contact["name"]) || !strlen($contact["name"]))
      && isset($contact["family_name"]) && strlen($contact["family_name"])
      && isset($contact["givennname"]) && strlen($contact["givennname"]))
      $contact["name"] = $contact["family_name"].' '.$contact["givennname"];
    if(!isset($contact["name"]) || !strlen($contact["name"])) return false;
    $query = 'SELECT uid FROM user WHERE name="'.$contact["name"].'"';
    $resulting_uids = $this->fql_query($query);
    
    $socializr = Socializr::getInstance();
    
    if($resulting_uids && is_array($resulting_uids)) foreach($resulting_uids as $resulting_uid) {
      //$results[$count] = $this->mapContactData($result);
      //$results[$count]["profile_url"] = "http://www.facebook.com/profile.php?id=".$result["uid"];
      $sparql_account = 'PREFIX foaf: <http://xmlns.com/foaf/0.1/> .
        ASK WHERE {?person foaf:holdsAccount <'.$socializr->getAccountUri($resulting_uid["uid"],"Facebook").'>}';
      if($socializr->_model->sparqlQuery($sparql_account)) continue; // account does already exist
      $result = $this->fetchContact($resulting_uid["uid"]);
      $result["profile_url"] = "http://www.facebook.com/people/" . $result["name"] . "/" . $resulting_uid["uid"]; 
      $results[] = $result;
    }
    return $results;
  }

  function fetchContacts() {
    if(!$this->connected()) $this->connect();
    $friends = $this->call_method('facebook.friends.get', array());

    return $friends;
  }

  function fetchPersonalData($uid) {
    if(!$this->connected()) $this->connect();
    $response = $this->call_method('facebook.users.getInfo', array('uids' => $uid, 'fields' => $this->available_fields));

    return (false == $response) ? null : $this->mapContactData(current($response));
  }
  
  function fetchContact($friend) {
    if(!$this->connected()) $this->connect();
    $personal_data = $this->fetchPersonalData($friend);
    if(null != $personal_data) {
      $this->contacts[$uid] = $personal_data; 

      return $this->contacts[$uid];
    }
    
    return false;
  }
  
  private function mapContactData($contactData) {
    $contact = array(
      'accountName'         => $contactData['uid'], // e-mail not available via facebook api
      'uid'                 => $contactData['uid'],
      'full_data'           => $contactData // for debugging and network specific issues: returns everything available
    );
    if(isset($contactData['birthday']) and 0 < strlen($contactData['birthday'])) {
      $contact['birthday'] = $contactData['birthday'];
    }
    if(isset($contactData['hometown_location']) and is_array($contactData['hometown_location'])) {
      $location = $contactData['hometown_location'];
      $contact['based_near'] = '';
      if(isset($location['zip']))     $contact['based_near'] = $location['zip'];
      if(isset($location['city']))    $contact['based_near'] = trim($contact['based_near'].' '.$location['zip']);
      if(isset($location['state']))   $contact['based_near'] = trim($contact['based_near'].' '.$location['state']);
      if(isset($location['country'])) $contact['based_near'] = trim($contact['based_near'].' '.$location['country']);
    }
    if(isset($contactData['first_name']) and 0 < strlen($contactData['first_name'])) {
      $contact['givenname'] = $contactData['first_name'];
    }
    if(isset($contactData['last_name']) and 0 < strlen($contactData['last_name'])) {
      $contact['family_name'] = $contactData['last_name'];
    }
    if(isset($contactData['name']) and 0 < strlen($contactData['name'])) {
      $contact['name'] = $contactData['name'];
    }
    if(isset($contactData['pic_big']) and 0 < strlen($contactData['pic_big'])) {
      $contact['depiction'] = $contactData['pic_big'];
    }
    if(isset($contactData['pic_small']) and 0 < strlen($contactData['pic_small'])) {
      $contact['thumbnail'] = $contactData['pic_small'];
    }
    if(isset($contactData['sex']) and 0 < strlen($contactData['sex'])) {
      $contact['gender'] = $contactData['sex'];
    }
    if(isset($contactData['timezone']) and 0 < strlen($contactData['timezone'])) {
      $contact['timezone'] = $contactData['timezone'];
    }
    if(isset($contactData['first_name']) and 0 < strlen($contactData['first_name'])) {
      $contact['givenname'] = $contactData['first_name'];
    }
    if(isset($contactData['first_name']) and 0 < strlen($contactData['first_name'])) {
      $contact['givenname'] = $contactData['first_name'];
    }
    $contact['socializr_account_uri']=$this->getAccountIdentifier($contact);
    return $contact;
  }

  /* seems not to be supported :( */  
  public function fetchContactsOfContacts($uid) {
    return false;
    $this->connect();
    $result = $this->facebook_client->api_client->fql_query('
      SELECT '.$this->available_fields.' FROM user
      WHERE uid IN (SELECT uid2 FROM friend WHERE uid1='.$uid.')
      AND "Facebook" IN affiliations.name AND uid < 10
    ');
  }
  
  public function areContacts($contact1, $contact2) {
    $this->connect();
    $result = $this->facebook_client->api_client->friends_areFriends($contact1->uid, $contact2->uid);
    return $result;
  }

  public function getAccountIdentifier($contact) {
    return $contact["uid"];
  }

  private function redirect($url) {
    if ($this->in_fb_canvas()) {
      echo '<fb:redirect url="' . $url . '"/>';
    } else if (preg_match('/^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?/i', $url)) {
      // make sure facebook.com url's load in the full frame so that we don't
      // get a frame within a frame.
      echo "<script type=\"text/javascript\">\ntop.location.href = \"$url\";\n</script>";
    } else {
      header('Location: ' . $url);
    }
    exit;
  }
  
  private static function get_facebook_url($subdomain='www') {
    return 'http://' . $subdomain . '.facebook.com';
  }
  
  private function & fql_query($query) { 
    return $this->call_method('facebook.fql.query',
      array('query' => $query));
  }
  
  private function & call_method($method, $params) {
    $xml = $this->post_request($method, $params);
    $result = $this->convert_xml_to_result($xml, $method, $params);

/*    if (is_array($result) && isset($result['error_code'])) { // to be catched later?
      throw new FacebookRestClientException($result['error_msg'], $result['error_code']);
    }*/
    return $result;
  }
  
  private function convert_xml_to_result($xml, $method, $params) {
    $sxml = simplexml_load_string($xml);
    $result = self::convert_simplexml_to_array($sxml);


    if (!empty($GLOBALS['facebook_config']['debug'])) {
      // output the raw xml and its corresponding php object, for debugging:
      print '<div style="margin: 10px 30px; padding: 5px; border: 2px solid black; background: gray; color: white; font-size: 12px; font-weight: bold;">';
      $this->cur_id++;
      print $this->cur_id . ': Called ' . $method . ', show ' .
            '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'params\');">Params</a> | '.
            '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'xml\');">XML</a> | '.
            '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'sxml\');">SXML</a> | '.
            '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'php\');">PHP</a>';
      print '<pre id="params'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($params, true).'</pre>';
      print '<pre id="xml'.$this->cur_id.'" style="display: none; overflow: auto;">'.htmlspecialchars($xml).'</pre>';
      print '<pre id="php'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($result, true).'</pre>';
      print '<pre id="sxml'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($sxml, true).'</pre>';
      print '</div>';
    }
    return $result;
  }

  private static function convert_simplexml_to_array($sxml) {
    $arr = array();
    if ($sxml) {
      foreach ($sxml as $k => $v) {
        if ($sxml['list']) {
          $arr[] = self::convert_simplexml_to_array($v);
        } else {
          $arr[$k] = self::convert_simplexml_to_array($v);
        }
      }
    }
    if (sizeof($arr) > 0) {
      return $arr;
    } else {
      return (string)$sxml;
    }
  }

  private function post_request($method, $params) {

    $post_string = $this->create_post_string($method, $params);

    if (function_exists('curl_init')) {
      // Use CURL if installed...
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $this->server_addr);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_USERAGENT, 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion());
      $result = curl_exec($ch);
      curl_close($ch);
    } else {
      // Non-CURL based version...
      $context =
        array('http' =>
              array('method' => 'POST',
                    'header' => 'Content-type: application/x-www-form-urlencoded'."\r\n".
                                'User-Agent: Facebook API PHP5 Client 1.1 (non-curl) '.phpversion()."\r\n".
                                'Content-length: ' . strlen($post_string),
                    'content' => $post_string));
      $contextid=stream_context_create($context);
      $sock=fopen($this->server_addr, 'r', false, $contextid);
      if ($sock) {
        $result='';
        while (!feof($sock))
          $result.=fgets($sock, 4096);

        fclose($sock);
      }
    }
    return $result;
  }

  private function create_post_string($method, $params) {
    $params['method']      = $method;
    if(0 < strlen(SocializrSession::getInstance()->getAttribute('facebook_session'))) {
      $params['session_key'] = SocializrSession::getInstance()->getAttribute('facebook_session');
    }
    $params['api_key']     = $this->api_key;
    $params['call_id']     = microtime(true);
    if ($params['call_id'] <= $this->last_call_id) {
      $params['call_id'] = $this->last_call_id + 0.001;
    }
    $this->last_call_id = $params['call_id'];
    if (!isset($params['v'])) {
      $params['v'] = '1.0';
    }
    $post_params = array();
    foreach ($params as $key => &$val) {
      if (is_array($val)) $val = implode(',', $val);
      $post_params[] = $key.'='.urlencode($val);
    }
    $secret = $this->secret;
    $post_params[] = 'sig='.self::generate_sig($params, $secret);
    return implode('&', $post_params);
  }

  private static function generate_sig($params_array, $secret) {
    $str = '';

    ksort($params_array);
    // Note: make sure that the signature parameter is not already included in
    //       $params_array.
    foreach ($params_array as $k=>$v) {
      $str .= "$k=$v";
    }
    $str .= $secret;

    return md5($str);
  }

  public function validate_fb_params() {
    $this->fb_params = $this->get_valid_fb_params($_POST, 48*3600, 'fb_sig');
    if (!$this->fb_params) {
      $this->fb_params = $this->get_valid_fb_params($_GET, 48*3600, 'fb_sig');
    }
    if ($this->fb_params) {
      // If we got any fb_params passed in at all, then either:
      //  - they included an fb_user / fb_session_key, which we should assume to be correct
      //  - they didn't include an fb_user / fb_session_key, which means the user doesn't have a
      //    valid session and if we want to get one we'll need to use require_login().  (Calling
      //    set_user with null values for user/session_key will work properly.)
      // Note that we should *not* use our cookies in this scenario, since they may be referring to
      // the wrong user.
      $user        = isset($this->fb_params['user'])        ? $this->fb_params['user'] : null;
      $session_key = isset($this->fb_params['session_key']) ? $this->fb_params['session_key'] : null;
      $expires     = isset($this->fb_params['expires'])     ? $this->fb_params['expires'] : null;
      $this->set_user($user, $session_key, $expires);
    } else if (!empty($_COOKIE) && $cookies = $this->get_valid_fb_params($_COOKIE, null, $this->api_key)) {
      // use $api_key . '_' as a prefix for the cookies in case there are
      // multiple facebook clients on the same domain.
      $this->set_user($cookies['user'], $cookies['session_key']);
    } else if (isset($_GET['auth_token']) && $session = $this->do_get_session($_GET['auth_token'])) {
      $this->set_user($session['uid'], $session['session_key'], $session['expires']);
    }

    return !empty($this->fb_params);
  }

  public function get_valid_fb_params($params, $timeout=null, $namespace='fb_sig') {
    $prefix = $namespace . '_';
    $prefix_len = strlen($prefix);
    $fb_params = array();
    foreach ($params as $name => $val) {
      if (strpos($name, $prefix) === 0) {
        $fb_params[substr($name, $prefix_len)] = self::no_magic_quotes($val);
      }
    }
    if ($timeout && (!isset($fb_params['time']) || time() - $fb_params['time'] > $timeout)) {
      return array();
    }
    if (!isset($params[$namespace]) || !$this->verify_signature($fb_params, $params[$namespace])) {
      return array();
    }
    return $fb_params;
  }
}


/**
 * Error codes and descriptions for the Facebook API.
 */
class FacebookAPIErrorCodes {

  const API_EC_SUCCESS = 0;

  /*
   * GENERAL ERRORS
   */
  const API_EC_UNKNOWN = 1;
  const API_EC_SERVICE = 2;
  const API_EC_METHOD = 3;
  const API_EC_TOO_MANY_CALLS = 4;
  const API_EC_BAD_IP = 5;

  /*
   * PARAMETER ERRORS
   */
  const API_EC_PARAM = 100;
  const API_EC_PARAM_API_KEY = 101;
  const API_EC_PARAM_SESSION_KEY = 102;
  const API_EC_PARAM_CALL_ID = 103;
  const API_EC_PARAM_SIGNATURE = 104;
  const API_EC_PARAM_USER_ID = 110;
  const API_EC_PARAM_USER_FIELD = 111;
  const API_EC_PARAM_SOCIAL_FIELD = 112;
  const API_EC_PARAM_ALBUM_ID = 120;

  /*
   * USER PERMISSIONS ERRORS
   */
  const API_EC_PERMISSION = 200;
  const API_EC_PERMISSION_USER = 210;
  const API_EC_PERMISSION_ALBUM = 220;
  const API_EC_PERMISSION_PHOTO = 221;

  const FQL_EC_PARSER = 601;
  const FQL_EC_UNKNOWN_FIELD = 602;
  const FQL_EC_UNKNOWN_TABLE = 603;
  const FQL_EC_NOT_INDEXABLE = 604;

  /**
   * DATA STORE API ERRORS
   */
  const API_EC_DATA_UNKNOWN_ERROR = 800;
  const API_EC_DATA_INVALID_OPERATION = 801;
  const API_EC_DATA_QUOTA_EXCEEDED = 802;
  const API_EC_DATA_OBJECT_NOT_FOUND = 803;
  const API_EC_DATA_OBJECT_ALREADY_EXISTS = 804;
  const API_EC_DATA_DATABASE_ERROR = 805;


  /*
   * Batch ERROR
   */
  const API_EC_BATCH_ALREADY_STARTED = 900;
  const API_EC_BATCH_NOT_STARTED = 901;
  const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 902;

  public static $api_error_descriptions = array(
      API_EC_SUCCESS           => 'Success',
      API_EC_UNKNOWN           => 'An unknown error occurred',
      API_EC_SERVICE           => 'Service temporarily unavailable',
      API_EC_METHOD            => 'Unknown method',
      API_EC_TOO_MANY_CALLS    => 'Application request limit reached',
      API_EC_BAD_IP            => 'Unauthorized source IP address',
      API_EC_PARAM             => 'Invalid parameter',
      API_EC_PARAM_API_KEY     => 'Invalid API key',
      API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid',
      API_EC_PARAM_CALL_ID     => 'Call_id must be greater than previous',
      API_EC_PARAM_SIGNATURE   => 'Incorrect signature',
      API_EC_PARAM_USER_ID     => 'Invalid user id',
      API_EC_PARAM_USER_FIELD  => 'Invalid user info field',
      API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field',
      API_EC_PARAM_ALBUM_ID    => 'Invalid album id',
      API_EC_PERMISSION        => 'Permissions error',
      API_EC_PERMISSION_USER   => 'User not visible',
      API_EC_PERMISSION_ALBUM  => 'Album not visible',
      API_EC_PERMISSION_PHOTO  => 'Photo not visible',
      FQL_EC_PARSER            => 'FQL: Parser Error',
      FQL_EC_UNKNOWN_FIELD     => 'FQL: Unknown Field',
      FQL_EC_UNKNOWN_TABLE     => 'FQL: Unknown Table',
      FQL_EC_NOT_INDEXABLE     => 'FQL: Statement not indexable',
      FQL_EC_UNKNOWN_FUNCTION  => 'FQL: Attempted to call unknown function',
      FQL_EC_INVALID_PARAM     => 'FQL: Invalid parameter passed in',
      API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error',
      API_EC_DATA_INVALID_OPERATION => 'Invalid operation',
      API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded',
      API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found',
      API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists',
      API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again',
      API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first',
      API_EC_BATCH_NOT_STARTED => 'end_batch called before start_batch',
      API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'this method is not allowed in batch mode',
  );
}
?>
